/*
 * File    : TrackerWebPageReplyImpl.java
 * Created : 08-Dec-2003
 * By      : parg
 *
 * Azureus - a Java Bittorrent client
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details ( see the LICENSE file ).
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.gudy.azureus2.pluginsimpl.local.tracker;

/**
 * @author parg
 *
 */

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPOutputStream;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.torrent.TOTorrent;
import org.gudy.azureus2.core3.torrent.TOTorrentAnnounceURLSet;
import org.gudy.azureus2.core3.torrent.TOTorrentException;
import org.gudy.azureus2.core3.torrent.TOTorrentFactory;
import org.gudy.azureus2.core3.tracker.host.TRHostTorrent;
import org.gudy.azureus2.core3.tracker.util.TRTrackerUtils;
import org.gudy.azureus2.core3.util.AsyncController;
import org.gudy.azureus2.core3.util.BEncoder;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimeFormatter;
import org.gudy.azureus2.core3.util.TorrentUtils;
import org.gudy.azureus2.plugins.tracker.TrackerTorrent;
import org.gudy.azureus2.plugins.tracker.web.TrackerWebPageResponse;

import com.aelitis.azureus.core.util.HTTPUtils;

public class TrackerWebPageResponseImpl implements TrackerWebPageResponse {
    private static final String NL = "\r\n";

    private ByteArrayOutputStream baos = new ByteArrayOutputStream(2048);

    private String content_type = "text/html";

    private int reply_status = 200;

    private Map<String, Object> header_map = new LinkedHashMap<String, Object>();

    private TrackerWebPageRequestImpl request;
    private boolean raw_output;
    private boolean is_async;

    private int explicit_gzip = 0; // not set, 1 = gzip, 2 = no gzip
    private boolean is_gzipped;

    protected TrackerWebPageResponseImpl(TrackerWebPageRequestImpl _request) {
        request = _request;

        String formatted_date_now = TimeFormatter.getHTTPDate(SystemTime.getCurrentTime());

        setHeader("Last-Modified", formatted_date_now);

        setHeader("Expires", formatted_date_now);
    }

    public void setLastModified(long time) {
        String formatted_date = TimeFormatter.getHTTPDate(time);

        setHeader("Last-Modified", formatted_date);
    }

    public void setExpires(long time) {
        String formatted_date = TimeFormatter.getHTTPDate(time);

        setHeader("Expires", formatted_date);
    }

    public void setContentType(String type) {
        content_type = type;
    }

    public void setReplyStatus(int status) {
        reply_status = status;
    }

    public void setHeader(String name, String value) {
        if (name.equalsIgnoreCase("set-cookie")) {

            Iterator<String> it = header_map.keySet().iterator();

            while (it.hasNext()) {

                String key = it.next();

                if (key.equalsIgnoreCase(name)) {

                    Object existing = header_map.get(key);

                    if (existing instanceof String) {

                        String old = (String) existing;

                        List l = new ArrayList(3);

                        l.add(old);
                        l.add(value);

                        header_map.put(name, l);

                    } else {

                        List l = (List) existing;

                        if (!l.contains(value)) {

                            l.add(value);
                        }
                    }

                    return;
                }
            }

            header_map.put(name, value);

        } else {

            addHeader(name, value, true);
        }
    }

    public void setGZIP(boolean gzip) {
        explicit_gzip = gzip ? 1 : 2;
    }

    protected String addHeader(String name, String value, boolean replace) {
        Iterator<String> it = header_map.keySet().iterator();

        while (it.hasNext()) {

            String key = it.next();

            if (key.equalsIgnoreCase(name)) {

                if (replace) {

                    it.remove();

                } else {

                    Object existing = header_map.get(key);

                    if (existing instanceof String) {

                        return ((String) existing);

                    } else if (existing instanceof List) {

                        List<String> l = (List<String>) existing;

                        if (l.size() > 0) {

                            return (l.get(0));
                        }
                    }

                    return (null);
                }
            }
        }

        header_map.put(name, value);

        return (value);
    }

    public OutputStream getOutputStream() {
        return (baos);
    }

    public OutputStream getRawOutputStream()

    throws IOException {
        raw_output = true;

        return (request.getOutputStream());
    }

    protected void complete()

    throws IOException {
        if (is_async || raw_output) {

            return;
        }

        byte[] reply_bytes = baos.toByteArray();

        // System.out.println( "TrackerWebPageResponse::complete: data = " + reply_bytes.length );

        String status_string = "BAD";

        // random collection

        if (reply_status == 200) {

            status_string = "OK";

        } else if (reply_status == 204) {

            status_string = "No Content";

        } else if (reply_status == 206) {

            status_string = "Partial Content";

        } else if (reply_status == 401) {

            status_string = "Unauthorized";

        } else if (reply_status == 404) {

            status_string = "Not Found";

        } else if (reply_status == 501) {

            status_string = "Not Implemented";
        }

        String reply_header = "HTTP/1.1 " + reply_status + " " + status_string + NL;

        // add header fields if not already present

        addHeader("Server", Constants.AZUREUS_NAME + " " + Constants.AZUREUS_VERSION, false);

        if (request.canKeepAlive()) {

            String applied_value = addHeader("Connection", "keep-alive", false);

            if (applied_value.equalsIgnoreCase("keep-alive")) {

                request.setKeepAlive(true);
            }
        } else {

            addHeader("Connection", "close", true);
        }

        addHeader("Content-Type", content_type, false);

        boolean do_gzip = false;

        if (explicit_gzip == 1 && !is_gzipped) {

            Map headers = request.getHeaders();

            String accept_encoding = (String) headers.get("accept-encoding");

            if (HTTPUtils.canGZIP(accept_encoding)) {

                is_gzipped = do_gzip = true;

                header_map.put("Content-Encoding", "gzip");
            }
        }

        Iterator<String> it = header_map.keySet().iterator();

        while (it.hasNext()) {

            String name = it.next();
            Object value = header_map.get(name);

            if (value instanceof String) {

                reply_header += name + ": " + value + NL;

            } else {

                List<String> l = (List<String>) value;

                for (String v : l) {

                    reply_header += name + ": " + v + NL;
                }
            }
        }

        if (do_gzip) {

            // try and set the content-length to that of the compressed data

            if (reply_bytes.length < 512 * 1024) {

                ByteArrayOutputStream temp = new ByteArrayOutputStream(reply_bytes.length);

                GZIPOutputStream gzos = new GZIPOutputStream(temp);

                gzos.write(reply_bytes);

                gzos.finish();

                reply_bytes = temp.toByteArray();

                do_gzip = false;
            }
        }

        reply_header += "Content-Length: " + reply_bytes.length + NL + NL;

        // System.out.println( "writing reply:" + reply_header );

        OutputStream os = request.getOutputStream();

        os.write(reply_header.getBytes());

        if (do_gzip) {

            GZIPOutputStream gzos = new GZIPOutputStream(os);

            gzos.write(reply_bytes);

            gzos.finish();

        } else {

            os.write(reply_bytes);
        }

        os.flush();
    }

    public boolean useFile(String root_dir, String relative_url)

    throws IOException {
        String target = root_dir + relative_url.replace('/', File.separatorChar);

        File canonical_file = new File(target).getCanonicalFile();

        // make sure some fool isn't trying to use ../../ to escape from web dir

        if (!canonical_file.toString().toLowerCase().startsWith(root_dir.toLowerCase())) {

            return (false);
        }

        if (canonical_file.isDirectory()) {

            return (false);
        }

        if (canonical_file.canRead()) {

            String str = canonical_file.toString().toLowerCase();

            int pos = str.lastIndexOf(".");

            if (pos == -1) {

                return (false);
            }

            String file_type = str.substring(pos + 1);

            FileInputStream fis = null;

            try {
                fis = new FileInputStream(canonical_file);

                useStream(file_type, fis);

                return (true);

            } finally {

                if (fis != null) {

                    fis.close();
                }
            }
        }

        return (false);
    }

    public void useStream(String file_type, InputStream input_stream)

    throws IOException {
        OutputStream os = getOutputStream();

        String response_type = HTTPUtils.guessContentTypeFromFileType(file_type);

        if (explicit_gzip != 2 && HTTPUtils.useCompressionForFileType(response_type)) {

            Map headers = request.getHeaders();

            String accept_encoding = (String) headers.get("accept-encoding");

            if (HTTPUtils.canGZIP(accept_encoding)) {

                is_gzipped = true;

                os = new GZIPOutputStream(os);

                header_map.put("Content-Encoding", "gzip");
            }
        }

        setContentType(response_type);

        byte[] buffer = new byte[4096];

        while (true) {

            int len = input_stream.read(buffer);

            if (len <= 0) {

                break;
            }

            os.write(buffer, 0, len);
        }

        if (os instanceof GZIPOutputStream) {

            ((GZIPOutputStream) os).finish();
        }
    }

    public void writeTorrent(TrackerTorrent tracker_torrent)

    throws IOException {
        try {

            TRHostTorrent host_torrent = ((TrackerTorrentImpl) tracker_torrent).getHostTorrent();

            TOTorrent torrent = host_torrent.getTorrent();

            // make a copy of the torrent

            TOTorrent torrent_to_send = TOTorrentFactory.deserialiseFromMap(torrent.serialiseToMap());

            // remove any non-standard stuff (e.g. resume data)

            torrent_to_send.removeAdditionalProperties();

            if (!TorrentUtils.isDecentralised(torrent_to_send)) {

                URL[][] url_sets = TRTrackerUtils.getAnnounceURLs();

                // if tracker ip not set then assume they know what they're doing

                if (host_torrent.getStatus() != TRHostTorrent.TS_PUBLISHED && url_sets.length > 0) {

                    // if the user has disabled the mangling of urls when hosting then don't do it here
                    // either

                    if (COConfigurationManager.getBooleanParameter("Tracker Host Add Our Announce URLs")) {

                        String protocol = torrent_to_send.getAnnounceURL().getProtocol();

                        for (int i = 0; i < url_sets.length; i++) {

                            URL[] urls = url_sets[i];

                            if (urls[0].getProtocol().equalsIgnoreCase(protocol)) {

                                torrent_to_send.setAnnounceURL(urls[0]);

                                torrent_to_send.getAnnounceURLGroup().setAnnounceURLSets(new TOTorrentAnnounceURLSet[0]);

                                for (int j = 1; j < urls.length; j++) {

                                    TorrentUtils.announceGroupsInsertLast(torrent_to_send, new URL[] { urls[j] });
                                }

                                break;
                            }
                        }
                    }
                }
            }

            baos.write(BEncoder.encode(torrent_to_send.serialiseToMap()));

            setContentType("application/x-bittorrent");

        } catch (TOTorrentException e) {

            Debug.printStackTrace(e);

            throw (new IOException(e.toString()));
        }
    }

    public void setAsynchronous(boolean a)

    throws IOException {
        AsyncController async_control = request.getAsyncController();

        if (async_control == null) {

            throw (new IOException("Request is not non-blocking"));
        }

        if (a) {

            is_async = true;

            async_control.setAsyncStart();

        } else {

            is_async = false;

            complete();

            async_control.setAsyncComplete();
        }
    }

    public boolean getAsynchronous() {
        return (is_async);
    }
}
